﻿/*
 * @author: wizardc
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace Dou.UI
{
    /// <summary>
    /// 支持图文混排和超链接的 UGUI 文本，只消耗 1 个 DC
    /// 
    /// 标签格式：
    /// [SpriteName] 精灵图片名称，后面可以跟多个参数；
    /// 按照图片工具生成规则，有2种精灵类型：
    /// 1. 单张精灵图片；
    /// 2. 多张精灵图片，按设定好的编号和速度来进行播放的精灵动画；
    /// 
    /// 标签支持的参数（注：可选参数顺序可以更改）：
    /// #S=50,50 - 图片宽高；
    /// #U - 下划线；
    /// #C=FF0000 - 颜色；
    /// #URL=Any Param - 超链接，等号后面为点击后传递的参数，配合图片使用；
    /// #URL=Any Param@Any Text - 超链接带文本形式，等号后面为点击后传递的参数，不能配合图片一起使用；
    /// 
    /// 示例：
    /// 1. [Anger] - 最简单的展示单个图片；
    /// 2. [Anger#U#URL=JumpToCity(200,200)] - 带下划线和超链接的单个图片；
    /// 3. [#U#C=FFFF00#URL=JumpToCity(200,200)@点击这里跳转] - 文本形式的带下划线的超链接；
    /// </summary>
    [ExecuteInEditMode]
    public class RichText : Text, IPointerClickHandler
    {
        [Serializable]
        public class HrefClickEvent : UnityEvent<string> { }

        private const string RegexTag = "\\[([0-9a-zA-Z]+)?(#[0-9a-zA-Z]+[^\\]#]*)?(#[0-9a-zA-Z]+[^\\]#]*)?(#[0-9a-zA-Z]+[^\\]#]*)?(#[0-9a-zA-Z]+[^\\]#]*)?\\]";

        private readonly StringBuilder _stringBuilder = new StringBuilder();
        private readonly Dictionary<int, SpriteInfo> _spriteInfos = new Dictionary<int, SpriteInfo>();
        private readonly List<HrefInfo> _hrefInfos = new List<HrefInfo>();
        private readonly List<UnderlineInfo> _underlineInfos = new List<UnderlineInfo>();
        private readonly UIVertex[] _tempVerts = new UIVertex[4];
        private readonly TagParser _tagParser = new TagParser();

        // 最终用于渲染的字符串
        private string _renderText = String.Empty;

        [SerializeField]
        private HrefClickEvent _onHrefClick = new HrefClickEvent();
        [SerializeField]
        private RichTextAsset _richTextAsset;

        /// <summary>
        /// 富文本资源配置
        /// </summary>
        public RichTextAsset richTextAsset
        {
            set
            {
                if (_richTextAsset != value)
                {
                    _richTextAsset = value;
                    material = _richTextAsset.material;
                }
            }
            get => _richTextAsset;
        }
        
        /// <summary>
        /// 点击超链接回调
        /// </summary>
        public HrefClickEvent OnHrefClick
        {
            set
            {
                _onHrefClick = value;
            }
            get
            {
                return _onHrefClick;
            }
        }

        public override float preferredWidth
        {
            get
            {
                var settings = GetGenerationSettings(Vector2.zero);
                return cachedTextGeneratorForLayout.GetPreferredWidth(_renderText, settings) / pixelsPerUnit;
            }
        }

        public override float preferredHeight
        {
            get
            {
                var settings = GetGenerationSettings(new Vector2(rectTransform.rect.size.x, 0.0f));
                return cachedTextGeneratorForLayout.GetPreferredHeight(_renderText, settings) / pixelsPerUnit;
            }
        }

        private void ParseText(string mText)
        {
            if (string.IsNullOrEmpty(mText) || !supportRichText)
            {
                _renderText = mText;
                return;
            }

            _stringBuilder.Clear();
            _spriteInfos.Clear();
            _hrefInfos.Clear();
            _underlineInfos.Clear();
            
            MatchCollection matches = Regex.Matches(mText, RegexTag);
            if (matches.Count > 0)
            {
                int textIndex = 0;
                for (int i = 0; i < matches.Count; i++)
                {
                    var match = matches[i];
                    if (!_tagParser.Parse(match, fontSize))
                    {
                        continue;
                    }

                    switch (_tagParser.type)
                    {
                        case TagType.Sprite:
                        {
                            if (richTextAsset.TryGetSprite(_tagParser.spriteName, out var info))
                            {
                                _stringBuilder.Append(mText.Substring(textIndex, match.Index - textIndex));
                                
                                int temIndex = _stringBuilder.Length;

                                _stringBuilder.Append("<quad size=");
                                _stringBuilder.Append(_tagParser.size.y);
                                _stringBuilder.Append(" width=");
                                _stringBuilder.Append((_tagParser.size.x / _tagParser.size.y).ToString("f2"));
                                _stringBuilder.Append(" />");

                                _spriteInfos.Add(temIndex, new SpriteInfo
                                {
                                    sprite = info,
                                    width = _tagParser.size.x,
                                    height = _tagParser.size.y
                                });

                                if (_tagParser.hasUrl)
                                {
                                    var hrefInfo = new HrefInfo
                                    {
                                        startIndex = temIndex * 4,
                                        endIndex = temIndex * 4 + 3,
                                        urlParam = _tagParser.urlParam,
                                    };
                                    _hrefInfos.Add(hrefInfo);
                                }

                                if (_tagParser.hasUnderline)
                                {
                                    var underlineInfo = new UnderlineInfo
                                    {
                                        startIndex = temIndex * 4,
                                        endIndex = temIndex * 4 + 3,
                                        color = _tagParser.color
                                    };
                                    _underlineInfos.Add(underlineInfo);
                                }

                                textIndex = match.Index + match.Length;
                            }
                            break;
                        }
                        case TagType.HyperLink:
                        {
                            _stringBuilder.Append(mText.Substring(textIndex, match.Index - textIndex));
                            var startIndex = _stringBuilder.Length;
                            
                            if (!string.IsNullOrEmpty(_tagParser.colorStr))
                            {
                                _stringBuilder.Append("<color=");
                                _stringBuilder.Append(_tagParser.colorStr);
                                _stringBuilder.Append(">");
                            }
                            
                            var href = new HrefInfo
                            {
                                startIndex = startIndex * 4,
                                urlParam = _tagParser.urlParam,
                            };
                            _hrefInfos.Add(href);

                            UnderlineInfo underlineInfo = null;
                            if (_tagParser.hasUnderline)
                            {
                                underlineInfo = new UnderlineInfo
                                {
                                    startIndex = _stringBuilder.Length * 4,
                                    color = _tagParser.color
                                };
                                _underlineInfos.Add(underlineInfo);
                            }
                            
                            _stringBuilder.Append(_tagParser.urlText);
                            
                            if (!string.IsNullOrEmpty(_tagParser.colorStr))
                            {
                                _stringBuilder.Append("</color>");
                            }

                            href.endIndex = _stringBuilder.Length * 4 - 1;
                            if (underlineInfo != null)
                            {
                                underlineInfo.endIndex = _stringBuilder.Length * 4 - 1;
                            }

                            textIndex = match.Index + match.Length;
                            break;
                        }
                    }
                }

                _stringBuilder.Append(mText.Substring(textIndex, mText.Length - textIndex));
                _renderText = _stringBuilder.ToString();
            }
            else
            {
                _renderText = mText;
            }
        }

        protected override void OnPopulateMesh(VertexHelper toFill)
        {
            if (font == null)
            {
                return;
            }
            
            if (string.IsNullOrEmpty(m_Text) || !supportRichText)
            {
                base.OnPopulateMesh(toFill);
                return;
            }

            ParseText(m_Text);

            // We don't care if we the font Texture changes while we are doing our Update.
            // The end result of cachedTextGenerator will be valid for this instance.
            // Otherwise we can get issues like Case 619238.
            m_DisableFontTextureRebuiltCallback = true;

            Vector2 extents = rectTransform.rect.size;

            var settings = GetGenerationSettings(extents);
            cachedTextGenerator.Populate(_renderText, settings);

            // Apply the offset to the vertices
            IList<UIVertex> verts = cachedTextGenerator.verts;
            float unitsPerPixel = 1 / pixelsPerUnit;
            int vertCount = verts.Count;

            // We have no verts to process just return (case 1037923)
            if (vertCount <= 0)
            {
                toFill.Clear();
                return;
            }
            
            Vector2 roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
            roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
            toFill.Clear();
            
            Vector3 repairVec = new Vector3(0, fontSize * 0.1f);
            Vector4 uv = Vector4.zero;
            for (int i = 0; i < vertCount; ++i)
            {
                int index = i / 4;
                int tempVertsIndex = i & 3;
                if (_spriteInfos.TryGetValue(index, out SpriteInfo info))
                {
                    _tempVerts[tempVertsIndex] = verts[i];
                    _tempVerts[tempVertsIndex].position -= repairVec;
                    
                    uv.x = info.sprite.index;
                    uv.y = info.sprite.frameCount;
                    _tempVerts[tempVertsIndex].uv0 += uv * 10;
                    
                    _tempVerts[tempVertsIndex].position *= unitsPerPixel;
                    if (roundingOffset != Vector2.zero)
                    {
                        _tempVerts[tempVertsIndex].position.x += roundingOffset.x;
                        _tempVerts[tempVertsIndex].position.y += roundingOffset.y;
                    }
                    if (tempVertsIndex == 3)
                    {
                        toFill.AddUIVertexQuad(_tempVerts);
                    }
                }
                else
                {
                    _tempVerts[tempVertsIndex] = verts[i];
                    _tempVerts[tempVertsIndex].position *= unitsPerPixel;
                    if (roundingOffset != Vector2.zero)
                    {
                        _tempVerts[tempVertsIndex].position.x += roundingOffset.x;
                        _tempVerts[tempVertsIndex].position.y += roundingOffset.y;
                    }
                    if (tempVertsIndex == 3)
                    {
                        toFill.AddUIVertexQuad(_tempVerts);
                    }
                }
            }
            
            CalcBoundsInfo(toFill, _hrefInfos);
            CalcBoundsInfo(toFill, _underlineInfos);
            DrawUnderline(toFill);
            
            m_DisableFontTextureRebuiltCallback = false;
        }

        /// <summary>
        /// 计算可点击的富文本部分的包围盒
        /// </summary>
        private void CalcBoundsInfo<T>(VertexHelper toFill, List<T> targetList) where T : HrefInfo
        {
            UIVertex vert = new UIVertex();
            for (int u = 0; u < targetList.Count; u++)
            {
                var href = targetList[u];
                href.boxes.Clear();
                if (href.startIndex >= toFill.currentVertCount)
                {
                    continue;
                }

                // 获取超链接开始时的顶点数据
                toFill.PopulateUIVertex(ref vert, href.startIndex);
                
                var pos = vert.position;
                var bounds = new Bounds(pos, Vector3.zero);
                // 获取当前这个超连接的所有顶点来确定点击区域
                for (int i = href.startIndex, m = href.endIndex; i < m; i++)
                {
                    if (i >= toFill.currentVertCount)
                    {
                        break;
                    }
                    // 获取顶点数据
                    toFill.PopulateUIVertex(ref vert, i);
                    pos = vert.position;
                    // 判定是否换行，换行新增新的点击区域
                    if (pos.x < bounds.min.x)
                    {
                        href.boxes.Add(new Rect(bounds.min, bounds.size));
                        bounds = new Bounds(pos, Vector3.zero);
                    }
                    else
                    {
                        // 根据顶点扩充点击区域
                        bounds.Encapsulate(pos);
                    }
                }

                href.boxes.Add(new Rect(bounds.min, bounds.size));
            }
        }

        /// <summary>
        /// 绘制下划线
        /// </summary>
        private void DrawUnderline(VertexHelper toFill)
        {
            if (_underlineInfos.Count <= 0)
            {
                return;
            }

            // 取出字符串 "_" 的顶点和高度信息
            Vector2 extents = rectTransform.rect.size;
            var settings = GetGenerationSettings(extents);
            cachedTextGenerator.Populate("_", settings);
            IList<UIVertex> uList = cachedTextGenerator.verts;
            float h = uList[2].position.y - uList[1].position.y;
            
            // 找出需要绘制下划线的区域，分别处理
            Vector3[] temVecs = new Vector3[4];
            for (int i = 0; i < _underlineInfos.Count; i++)
            {
                var info = _underlineInfos[i];

                for (int j = 0; j < info.boxes.Count; j++)
                {
                    if (info.boxes[j].width <= 0 || info.boxes[j].height <= 0)
                    {
                        continue;
                    }

                    // 每个下划线区域用4个顶点来模拟，通过基于上方生成的"_"符号的顶点即可，y轴下移h高度
                    temVecs[0] = info.boxes[j].min;
                    temVecs[1] = temVecs[0] + new Vector3(info.boxes[j].width, 0);
                    temVecs[2] = temVecs[0] + new Vector3(info.boxes[j].width, -h);
                    temVecs[3] = temVecs[0] + new Vector3(0, -h);

                    for (int k = 0; k < 4; k++)
                    {
                        _tempVerts[k] = uList[k];
                        _tempVerts[k].color = info.color;
                        _tempVerts[k].position = temVecs[k];
                        _tempVerts[k].uv0 = GetUnderlineCharUV(); // 计算UV
                    }

                    toFill.AddUIVertexQuad(_tempVerts);
                }
            }
        }

        private Vector2 GetUnderlineCharUV()
        {
            if (font.GetCharacterInfo('_', out CharacterInfo info, fontSize, fontStyle))
            {
                return (info.uvBottomLeft + info.uvBottomRight + info.uvTopLeft + info.uvTopRight) * 0.25f;
            }
            return Vector2.zero;
        }

        public void OnPointerClick(PointerEventData eventData)
        {
            RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, eventData.position, eventData.pressEventCamera, out Vector2 lp);
            for (int h = 0; h < _hrefInfos.Count; h++)
            {
                var hrefInfo = _hrefInfos[h];
                var boxes = hrefInfo.boxes;
                for (var i = 0; i < boxes.Count; ++i)
                {
                    if (boxes[i].Contains(lp))
                    {
                        OnHrefClick.Invoke(hrefInfo.urlParam);
                        return;
                    }
                }
            }
        }

        private enum TagType
        {
            None,
            Sprite,
            HyperLink,
        }

        private class TagParser
        {
            private const string SizeTag = "^#S=([\\.0-9]+),([\\.0-9]+)$";
            private const string UnderlineTag = "^#U$";
            private const string ColorTag = "^#C=([0-9a-fA-F]{6})$";
            private const string UrlTag = "^#URL=([^@]+)@?([\\s\\S]*)$";
            
            public TagType type;
            public string spriteName;
            public Vector2 size;
            public bool hasUnderline;
            public string colorStr;
            public Color color;
            public bool hasUrl;
            public string urlParam;
            public string urlText;
            
            void Reset()
            {
                type = TagType.None;
                spriteName = string.Empty;
                size = Vector2.zero;
                hasUnderline = false;
                colorStr = string.Empty;
                color = Color.white;
                hasUrl = false;
                urlParam = string.Empty;
                urlText = string.Empty;
            }

            public bool Parse(Match match, int fontSize)
            {
                Reset();
                if (!match.Success || match.Groups.Count != 6) // 支持1个精灵名称加4个参数，必然会有6个组
                {
                    return false;
                }

                var groups = match.Groups;

                var paramStartIndex = 1;
                if (!string.IsNullOrEmpty(groups[1].Value))
                {
                    type = TagType.Sprite;
                    spriteName = groups[1].Value;
                    paramStartIndex = 2;
                }
                else
                {
                    type = TagType.HyperLink;
                }

                // 解析参数
                for (int i = paramStartIndex; i < groups.Count; i++)
                {
                    var value = groups[i].Value;
                    if (!string.IsNullOrEmpty(value))
                    {
                        if (Regex.IsMatch(value, SizeTag, RegexOptions.IgnoreCase))
                        {
                            var m = Regex.Match(value, SizeTag, RegexOptions.IgnoreCase);
                            size.Set(float.Parse(m.Groups[1].Value), float.Parse(m.Groups[2].Value));
                        }
                        else if (Regex.IsMatch(value, UnderlineTag, RegexOptions.IgnoreCase))
                        {
                            hasUnderline = true;
                        }
                        else if (Regex.IsMatch(value, ColorTag, RegexOptions.IgnoreCase))
                        {
                            var m = Regex.Match(value, ColorTag, RegexOptions.IgnoreCase);
                            colorStr = "#" + m.Groups[1].Value;
                            ColorUtility.TryParseHtmlString(colorStr, out color);
                        }
                        else if (Regex.IsMatch(value, UrlTag, RegexOptions.IgnoreCase))
                        {
                            hasUrl = true;
                            var m = Regex.Match(value, UrlTag, RegexOptions.IgnoreCase);
                            urlParam = m.Groups[1].Value;
                            urlText = m.Groups[2].Value;
                        }
                    }
                }

                if (size == Vector2.zero)
                {
                    size.Set(fontSize, fontSize);
                }
                
                if (type == TagType.HyperLink && string.IsNullOrEmpty(urlText))
                {
                    return false;
                }

                return true;
            }
        }

        private class SpriteInfo
        {
            public SpriteData sprite;
            public float width;
            public float height;
        }

        private class HrefInfo
        {
            public int startIndex;
            public int endIndex;
            public string urlParam;
            public readonly List<Rect> boxes = new List<Rect>();
        }

        private class UnderlineInfo : HrefInfo
        {
            public Color color;
        }
    }
}
